# Copyright (C) 2008, 2010 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

__author__ = 'carterm@google.com (Carter Maslan)'

import cgi
import wsgiref.handlers
import urllib
import os
import getopt
import sys
import string
import time
import datetime
import re
try:
  from xml.etree import ElementTree
except ImportError:
  from elementtree import ElementTree
import csv
from csv import QUOTE_ALL, QUOTE_MINIMAL, QUOTE_NONE, QUOTE_NONNUMERIC
import StringIO

import atom
import atom.service
import gdata.urlfetch
import gdata.service
import gdata.calendar
import gdata.calendar.service

gdata.service.http_request_handler = gdata.urlfetch
from google.appengine.api import users
from google.appengine.ext import webapp
from google.appengine.ext import db
from google.appengine.ext.webapp import template
from django.utils import simplejson
from google.appengine.ext.webapp import util


FORMAT_STRING_TIMESTAMP = "%Y-%m-%dT%H:%M:%S"
FORMAT_STRING_DAY = "%Y-%m-%d"

port = os.environ['SERVER_PORT']
if port and port != '80':
  HOST_NAME = '%s:%s' % (os.environ['SERVER_NAME'], port)
else:
  HOST_NAME = os.environ['SERVER_NAME']


class Calendar(db.Model):
  title = db.StringProperty()
  id = db.LinkProperty()
  tail = db.StringProperty()
  color = db.StringProperty()


class CalendarEntry(db.Model):
  title = db.StringProperty()
  id = db.LinkProperty()
  content = db.StringProperty()  
  where = db.StringProperty()
  start_time = db.DateTimeProperty()
  end_time = db.DateTimeProperty()
  duration = db.FloatProperty()
  rate = db.FloatProperty()
  fee = db.FloatProperty()
  payment = db.FloatProperty()
  content_stripped = db.StringProperty()


class UserSettings(db.Model):
  user_email = db.StringProperty()
  statement_header = db.TextProperty(default='Click <a href="/settings">Settings</a> edit this statement header.<br/><br/><b>Your Name</b><br/>Mountain View, CA<br/><br/><hr>')
  hourly_rate = db.FloatProperty(default=float(100.0))
  last_calendar_id = db.TextProperty()


# class to wrap context for gdata and app engine calls
class AuthContext(object):
	
  def __init__(self, requestURI, requestQuery, appDomain):
    self.request_uri = requestURI
    self.request_query = requestQuery
    self.app_domain = appDomain
    self.current_user = None
    # Stores the Google Data Client
    self.client = gdata.calendar.service.CalendarService()
    gdata.alt.appengine.run_on_appengine(self.client)
    # Stores the feed we're fetching.
    self.feed_url = 'http://www.google.com/calendar/feeds/'
    # url for the signin/signout
    self.url_auth = None
    self.url_authsub = None
    self.url_linktext = None

    session_token = None
    # The one time use token value from the URL after the AuthSub redirect.
    auth_token = gdata.auth.extract_auth_sub_token_from_url(self.request_uri)

    self._GetCurrentUser()

    if auth_token:
      try:
        session_token = self.client.upgrade_to_session_token(auth_token)
      except gdata.service.RequestError, token_upgrade_error:
        session_token = None

    if session_token:
      self.client.SetAuthSubToken(session_token)

    # Check to see if the app has permission to write to the user's 
    # Google Calendar.
    if not isinstance(self.client.token_store.find_token(self.feed_url),
        gdata.auth.AuthSubToken):
      self.url_authsub = gdata.auth.generate_auth_sub_url(self.request_uri,
         (self.feed_url,))
    #TODO(carterm) remove this else duplication if users can deny access via their account settings?
    else:
      self.url_authsub = gdata.auth.generate_auth_sub_url(self.request_uri,
         (self.feed_url,))

    if session_token and self.current_user:
      self.client.token_store.add_token(session_token)
    elif session_token:
      self.client.current_token = session_token


  def _GetCurrentUser(self):
    if users.get_current_user():
      self.url_auth = users.create_logout_url(self.request_uri)
      self.url_linktext = 'Sign out'
      self.current_user = users.get_current_user()
    else:
      self.url_auth = users.create_login_url(self.request_uri)
      self.url_linktext = 'Sign in'
      self.current_user = ''


class MainPageHandler(webapp.RequestHandler):

  def __init__(self):
    self.auth_context = None
    self.rate = None
    self.query = ''
    self.start_date = ''
    self.end_date = ''
    self.calendar_id = None
    # Stores the messages to display to user.
    self.message_text = None
    # Stores list of calendars
    self.calendar_list = []


  def get(self):
    self.auth_context = AuthContext(self.request.uri, self.request.query, HOST_NAME)

    for param in self.request.query.split('&'):
      if param.startswith('r'):
        self.rate = float(urllib.unquote_plus(param.split('=')[1]))
      elif param.startswith('q'):
        self.query = urllib.unquote_plus(param.split('=')[1])
      elif param.startswith('s'):
        self.start_date = urllib.unquote_plus(param.split('=')[1])
      elif param.startswith('e'):
        self.end_date = urllib.unquote_plus(param.split('=')[1])
      elif param.startswith('c'):
        self.calendar_id = urllib.unquote_plus(param.split('=')[1])

    self._FetchFeed()

    user_settings = None
    if self.auth_context.current_user:
      us = UserSettings.gql('WHERE user_email = :1', users.get_current_user().email())
      for s in us:   #TODO(carterm)
	    user_settings = s

    if not user_settings:   #create default settings
      user_settings = UserSettings()

    if self.rate:   #override settings default with url parameter if present
	  user_settings.hourly_rate = self.rate	  

    if not self.calendar_id:
      self.calendar_id = user_settings.last_calendar_id

    template_values = {
      'url': self.auth_context.url_auth,
      'url_linktext': self.auth_context.url_linktext,
      'current_user' : self.auth_context.current_user,
      'url_authsub' : self.auth_context.url_authsub,
      'message_text' : self.message_text,
      'calendar_list' : self.calendar_list,
      'settings' : user_settings,
      'query' : self.query,
      'start_date' : self.start_date,
      'end_date' : self.end_date,
      'calendar_id' : self.calendar_id,
      }

    path = os.path.join(os.path.dirname(__file__), 'templates/index.html')
    self.response.out.write(template.render(path, template_values))


  def _FetchFeed(self):
    try:
      self._FillCalenarList()
    except gdata.service.RequestError, request_error:
      # If fetching fails, then tell the user that they need to login to
      # authorize this app by logging in at the following URL.
      if request_error[0]['status'] == 401:
        # Get the URL of the current page so that our AuthSub request will
        # send the user back to here.
        if not self.auth_context.current_user:
          self.message_text = 'First <a href="%s">Sign-in</a> and then <a href="%s">Authorize</a> gTime to read your Google Calendars.' % (self.auth_context.url_auth, self.auth_context.url_authsub)
        else:
          self.message_text = 'Please <a href="%s">Authorize</a> gTime to read your Google Calendars. (status: %s)' % (self.auth_context.url_authsub, str(request_error[0]['status']))
      else:
        self.message_text = 'Oops; something went wrong.  The error is: %s ' % (str(request_error[0]))


  def _FillCalenarList(self):
    calendar_service = self.auth_context.client
    feed = calendar_service.GetAllCalendarsFeed()
    # print feed.title.text
    for i, a_calendar in enumerate(feed.entry):
      c = Calendar()
      c.title = a_calendar.title.text
      c.id = a_calendar.id.text
      s = c.id.split('/full/')
      c.tail = s[(len(s)-1)]
      c.color = a_calendar.color.value
      self.calendar_list.append(c)


class RPCHandler(webapp.RequestHandler):
  def __init__(self):
    webapp.RequestHandler.__init__(self)
    self.methods = RPCMethods()
    self.auth_context = None

  def get(self):
    self.auth_context = AuthContext(self.request.uri, self.request.query, HOST_NAME)
    func = None

    action = self.request.get('action')
    if action:
      if action[0] == '_':
        self.error(403) # access denied
        return
      else:
        func = getattr(self.methods, action, None)

    if not func:
      self.error(404) # file not found
      return

    args = ()
    while True:
      key = 'arg%d' % len(args)
      val = self.request.get(key)
      if val:
        args += (simplejson.loads(val),)
      else:
        break
    result = func(*args)
    self.response.out.write(simplejson.dumps(result))


  def post(self):
    self.auth_context = AuthContext(self.request.uri, self.request.query, HOST_NAME)

    args = simplejson.loads(self.request.body)
    func, args = args[0], args[1:]

    if func[0] == '_':
      self.error(403) # access denied
      return

    funcObj = getattr(self.methods, func, None)
    if not funcObj:
      self.error(404) # file not found
      return

    result = funcObj(self.auth_context, *args)
    self.response.out.write(simplejson.dumps(result))


class RPCMethods(object):
  """ Defines the methods that can be RPCed.
  NOTE: Do not allow remote callers access to private/protected "_*" methods.
  """

  def SaveSettings(self, *args):
    settings = args[1]
    try:
      sh = settings['statement_header']
      hr = float(settings['hourly_rate'])
      user_settings = UserSettings(user_email=users.get_current_user().email(),
                                 statement_header=sh,
                                 hourly_rate=hr)

      us = UserSettings.gql('WHERE user_email = :1', users.get_current_user().email())
      for s in us:   #TODO(carterm)
	    user_settings = s

      user_settings.statement_header = sh #TODO(carterm) escape/validate good html
      user_settings.hourly_rate = hr
      user_settings.put()
      return 'Settings saved successfully:<br/>' + user_settings.statement_header
    except:
      return 'Settings NOT saved. Please check that your entries contain valid values and try again.'
	

  def _StoreLastCalendarId(self, authContext, calendarId):
    if (authContext.current_user):
      user_email = authContext.current_user.email()
    if (user_email):
      user_settings = None
      us = UserSettings.gql('WHERE user_email = :1', user_email)
      for s in us:   #TODO(carterm)
	    user_settings = s

      if user_settings:
        if user_settings.last_calendar_id != calendarId:
          user_settings.last_calendar_id = calendarId
          user_settings.put()
      else:
        user_settings = UserSettings(user_email=user_email, last_calendar_id=calendarId)
        user_settings.put()


  def Search(self, *args):
    auth_context = args[0]
    results = []
    totals = []
    sr = args[1]
    self._StoreLastCalendarId(auth_context, sr['calendarId'])
    self._DateRangeQuery(auth_context.client, results, totals, sr['calendarId'], sr['startDate'], sr['endDate'], sr['queryString'], sr['defaultRate'])

    template_values = {
      'result_list' : results,
      'total_list' : totals,
      }
    path = os.path.join(os.path.dirname(__file__), 'templates/searchresults.html')
    return template.render(path, template_values)

  def _DateRangeQuery(self, client, result_list, total_list, calendar_id='default', start_date='2008-07-01', end_date='2008-08-01', text_query='*', default_rate='100'):
    """Retrieves list of CalendarEntry from the server which occur during the specified date
    range and text_query.  This uses the CalendarEventQuery class to generate the URL which is
    used to retrieve the feed.  For more information on valid query parameters,
    see: http://code.google.com/apis/calendar/reference.html#Parameters
    """
    #TODO(carterm) create a python unescape rather than using hard-coded replace() below
    query = gdata.calendar.service.CalendarEventQuery(calendar_id.replace('%40', '@'), 'private', 'full', text_query)
    # new per http://code.google.com/appengine/articles/more_google_data.html#gcal
    gdata.alt.appengine.run_on_appengine(query)

    #TODO(carterm) don't hardcode timezone
    query.start_min = start_date + 'T00:00:00-07:00'
    query.start_max = end_date + 'T23:59:59-07:00' 
    query.singleevents = "true"
    query.orderby = "starttime"
    query.sortorder = "descending"
    query.max_results = 200
    feed = client.CalendarQuery(query)

    total_hours = 0.0
    total_fees = 0.0
    total_payments = 0.0

    # regex pattern for rate: 999.99
    p_rate = re.compile(r'(rate:\s*)(\d*\.*\d*)', re.IGNORECASE)
    for i, an_event in zip(xrange(len(feed.entry)), feed.entry):
      ce = CalendarEntry()
      ce.title = an_event.title.text
      try:
        ce.content = an_event.content.text
      except:
	    ce.content = ''
      if not ce.content:
        ce.content = ''

      m = p_rate.search(ce.content)
      if m and len(m.groups()) == 2:
        ce.rate = float(m.groups()[1])
      else:
        ce.rate = float(default_rate)
      # add a version of the content that's stripped of regex pattern matches
      ce.content_stripped = p_rate.sub('', ce.content)

      #TODO(carterm) - there could be more than 1 WHERE
      for a_where in an_event.where:
        ce.where = a_where.value_string
      if not ce.where:
        ce.where = ''

      for a_when in an_event.when:
        ce.start_time = self._DateTimeFromString(a_when.start_time)
        ce.end_time = self._DateTimeFromString(a_when.end_time)
        delta = ce.end_time - ce.start_time
        if delta.days > 0:
          seconds = (delta.days * 24 * 3600) + delta.seconds
        else:
          seconds = delta.seconds
        ce.duration = float(seconds)/3600
        ce.fee = ce.duration * ce.rate
        ce.payment = 0.00
        total_hours += ce.duration
        total_fees += ce.fee
        total_payments += ce.payment
      result_list.append(ce)
    total_list.extend([total_hours, total_fees, total_payments])
    return result_list

  def _DateTimeFromString(self, date_string):
    """Returns a datetime from a string formatted in either FORMAT_STRING_DAY
    or FORMAT_STRING_TIMESTAMP
    """
    #TODO(carterm) - add robust support for all kinds of date_string formats
    if len(date_string) < 29:
      r = datetime.datetime.strptime(date_string,FORMAT_STRING_DAY)
    else:
      r = datetime.datetime.strptime(date_string[0:-10],FORMAT_STRING_TIMESTAMP)
    return r


class DownloadHandler(webapp.RequestHandler):

  def post(self):
    statement = simplejson.loads(self.request.get('jsonExport'))
    fmt = self.request.get('downloadFormat')
    if fmt == 'XML':
      self.response.headers['Content-Type'] = 'text/xml'
      self.response.headers['Content-Disposition'] = 'attachment; filename=statement.xml'
      statement['header'] = 'test xml'
      template_values = {
        'statement' : statement,
        }
      path = os.path.join(os.path.dirname(__file__), 'templates/statement.xml')
      self.response.out.write(template.render(path, template_values))
    else:
      self.response.headers['Content-Type'] = 'text/csv'
      self.response.headers['Content-Disposition'] = 'attachment; filename=statement.csv'
      self.response.out.write(self._dumps(statement['lines']))

  def _dumps(self, o):
    buffer = StringIO.StringIO()
    writer = csv.writer(buffer)
    for lineNumber, row in enumerate(o):
        if lineNumber == 0:
            # Print header row
            fields = row.keys()
            writer.writerow(fields)
        csvRow = []
        for f in fields:
            csvRow.append(row[f])
        writer.writerow(csvRow)
    return buffer.getvalue()


class StatementHandler(webapp.RequestHandler):

  def post(self):
    statement = simplejson.loads(self.request.get('jsonStatement'))

    user_settings = None
    us = UserSettings.gql('WHERE user_email = :1', users.get_current_user().email())
    for s in us:   #TODO(carterm)
	  user_settings = s

    if user_settings:
      statement['header'] = user_settings.statement_header

    template_values = {
      'statement' : statement,
      }

    path = os.path.join(os.path.dirname(__file__), 'templates/statementPage.html')
    self.response.out.write(template.render(path, template_values))


class SettingsHandler(webapp.RequestHandler):
  def __init__(self):
    self.auth_context = None

  def get(self):
    self.auth_context = AuthContext(self.request.uri, self.request.query, HOST_NAME)

    user_settings = None
    if self.auth_context.current_user:
      user_settings = UserSettings(user_email=users.get_current_user().email())
      us = UserSettings.gql('WHERE user_email = :1', users.get_current_user().email())
      for s in us:   #TODO(carterm) don't loop for this
	    user_settings = s

    template_values = {
      'url': self.auth_context.url_auth,
      'url_linktext': self.auth_context.url_linktext,
      'current_user' : self.auth_context.current_user,
      'url_authsub' : self.auth_context.url_authsub,
      'settings' : user_settings,
      }

    path = os.path.join(os.path.dirname(__file__), 'templates/settings.html')
    self.response.out.write(template.render(path, template_values))


class HelpHandler(webapp.RequestHandler):
  def __init__(self):
    self.auth_context = None

  def get(self):
    self.auth_context = AuthContext(self.request.uri, self.request.query, HOST_NAME)

    user_settings = None
    if self.auth_context.current_user:
      user_settings = UserSettings(user_email=users.get_current_user().email())
      us = UserSettings.gql('WHERE user_email = :1', users.get_current_user().email())
      for s in us:   #TODO(carterm) don't loop for this
	    user_settings = s

    template_values = {
      'url': self.auth_context.url_auth,
      'url_linktext': self.auth_context.url_linktext,
      'current_user' : self.auth_context.current_user,
      'url_authsub' : self.auth_context.url_authsub,
      'settings' : user_settings,
      }

    path = os.path.join(os.path.dirname(__file__), 'templates/help.html')
    self.response.out.write(template.render(path, template_values))


def main():
  application = webapp.WSGIApplication([
    (r'/', MainPageHandler),
    (r'/rpc', RPCHandler),
    (r'/settings', SettingsHandler),
    (r'/download', DownloadHandler),
    (r'/statement', StatementHandler),
    (r'/help', HelpHandler),
    ], debug=True)
  util.run_wsgi_app(application)

if __name__ == '__main__':
  main()